//
// Mouse Gesture Recognizer
// (c) Robert Hahn / HTW Dresden
//
// NetworkBackPropagation.cs
// -------------------------
// Neural network using the backpropagation algorithm.
//

using System;
using System.Threading;
using System.Collections;
using System.ComponentModel;
using System.IO;
using System.Diagnostics;

using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Soap;


namespace CNeural
{
	/// <summary>
	/// Layered fully connected feedforward network.
	/// </summary>
	[TypeConverter(typeof(ExpandableObjectConverter))]
	[Serializable]
	public class NetworkBackPropagation : NetworkNeural, ISerializable
	{
		#region Fields
		protected LayerList layers;
		protected double momentum;
		protected NeuralPatternList trainingPatterns;
		protected bool isLearning;
		protected int numberOfHiddenLayers;
		private INeuralPattern currentPattern;

	
		#endregion

		#region Events
		public override event EventHandler StepFinished;
		public override event EventHandler LearningFinished;
		#endregion

		
		#region Constructors
		public NetworkBackPropagation() : base()
		{
			this.layers = new LayerList();
			this.trainingPatterns = new NeuralPatternList();
			this.momentum = 0.5F;
			this.learnRate = 0.5F;
			
		}

		/// <summary>
		/// Serialization constructor.
		/// </summary>
		/// <param name="info"></param>
		/// <param name="context"></param>
		public NetworkBackPropagation(SerializationInfo info, 
			StreamingContext context) :this()
		{
			this.layers = (LayerList) info.GetValue("Layers", typeof(LayerList));
			this.momentum = (double)info.GetDouble("Momentum");
			this.learnRate = (double)info.GetDouble("LearnRate");
			this.errorLimit = (double) info.GetDouble("ErrorLimit");
			this.maximumIterations = info.GetInt32("MaximumIterations");
			
		}
		#endregion

		#region Properties

		public NeuralPatternList TrainingPatterns
		{
			get
			{
				return this.trainingPatterns;
			}
			set
			{
				this.trainingPatterns = value;
			}
		}
		public INeuralPattern[] TrainingPatternArray
		{
			get
			{
				NeuralPattern[] patterns = (NeuralPattern[]) 
					trainingPatterns.ToArray(typeof(NeuralPattern));
				return patterns;
			}
			set
			{
				
				if (this.trainingPatterns == null)
				{
					this.trainingPatterns = new NeuralPatternList();
				}
				else 
				{
					this.trainingPatterns.Clear();
				}
				foreach (INeuralPattern p in trainingPatterns)
				{			
					this.trainingPatterns.Add(p);
				}

			}
		}
	
		public INeuralPattern CurrentPattern
		{
			get
			{
				if(currentPattern == null)
					currentPattern = this.trainingPatterns[0];

				return this.currentPattern;
			}
		}
		/// <summary>
		/// Gets or sets the layers of the network.
		/// </summary>
		public LayerList Layers 
		{
			get 
			{ 
				return this.layers; 
			}
			set 
			{ 
				this.layers = value; 
			}
		}


		/// <summary>
		/// Gets or sets the momentum.
		/// </summary>
		public double Momentum 
		{
			get 
			{ 
				return this.momentum; 
			}
			set 
			{ 
				this.momentum = value; 
			}
		}
		/// <summary>
		/// Determines if the network is learning at the moment.
		/// </summary>
		public bool IsLearning
		{
			get 
			{ 
				return this.isLearning; 
			}
			set 
			{ 
				this.isLearning = value; 
			}
		}
		
		public int NumberOfHiddenLayers
		{
			get { return this.numberOfHiddenLayers; }
			set { this.numberOfHiddenLayers = value; }
		}
		#endregion

		

		#region Methods
		
		
		/// <summary>
		/// Connect all layers of the network.
		/// </summary>
		public void Connect() 
		{
			// for each layer
			for (int i = 0; i < layers.Count - 1; i++) 
			{
				// for each neuron in the layer
				for (int j = 0; j < layers[i].Count; j++) 
				{
					// for each neuron in the next layer
					for (int k = 0; k < layers[i + 1].Count; k++) 
					{
						Neuron n1 = layers[i][j];
						Neuron n2 = layers[i + 1][k];
						n2.Connect(n1);
					}
				}
			}
			this.IsConnected = true;
		}


		public void InitializeNewNetwork(int inp, int hid, int output)
		{
			this.Layers.Clear();
			this.AddLayer(LayerType.Input, inp, false);
			Debug.Assert(this.numberOfHiddenLayers > 0);
			
			for(int i = 0; i < this.numberOfHiddenLayers; i++)
                this.AddLayer(LayerType.Hidden, hid, true);

			// HACK test with 2 hidden layers
			//this.network.AddLayer(LayerType.Hidden, neuronNumber, true);
			this.AddLayer(LayerType.Output, output, true);

		}
		/// <summary>
		/// Adds a new neuron layer to the network.
		/// </summary>
		/// <param name="type">Type of the layer.</param>
		/// <param name="neuronNumber">Number of neurons in the layer.</param>
		private void AddLayer(LayerType type, int neuronNumber, bool isUsingBias) 
		{
			NeuronLayer l = new NeuronLayer(type);
			Neuron n;
			switch (type) 
			{
				// TODO: make sure only one input and one output layer exists
				case LayerType.Input:
					for (int i = 0; i < neuronNumber; i++) 
					{
						n = new Neuron(l, i, null, null, false, this.randomGenerator);
						l.Add(n);
					}
					break;

				case LayerType.Hidden:
					//this.hiddenLayerSize = value; 
					
					for (int i = 0; i < neuronNumber; i++)
					{
						n = new Neuron(l, i, new SumInputFunction(), new SigmoidFunction(), true, this.randomGenerator);
						l.Add(n);
					}
							
						
					
					this.isConnected = false;
					break;
				case LayerType.Output:
					for (int i = 0; i < neuronNumber; i++) 
					{					
						n = new Neuron(l, i, new SumInputFunction(), new SigmoidFunction(), true, this.randomGenerator);
						l.Add(n);
					}
					break;
			}			
			this.layers.Add(l);
		}


		public void SimulateNetwork(INeuralPattern p )
		{
			this.currentPattern = p;
			FeedForward(p.Input) ;
		}
		public ArrayList GetSimulationResults()
		{
			ArrayList trResults = new ArrayList();
			NeuronLayer outputLayer = this.layers[this.layers.Count - 1];
			if (outputLayer != null) 
			{
				double[] darr = outputLayer.Output;

				for(int i = 0; i < outputLayer.Output.GetLength(0); i++)
				{
					double d = outputLayer.Output[i];
					trResults.Add(d);
					//System.Diagnostics.Debug.WriteLine("Output " + d.ToString());

				}
					
			}
			return trResults;

		}
		public ArrayList GetTrainingResults()
		{
			ArrayList trResults = new ArrayList();
			//test:
			//System.Diagnostics.Debug.WriteLine("Iterations " + this.currentIteration);

			foreach (INeuralPattern p in trainingPatterns) 
			{
				FeedForward(p.Input);
				NeuronLayer outputLayer = this.layers[this.layers.Count - 1];
				if (outputLayer != null) 
				{
					double[] darr = outputLayer.Output;

					for(int i = 0; i < outputLayer.Output.GetLength(0); i++)
					{
						double d = outputLayer.Output[i];
						trResults.Add(d);
						//System.Diagnostics.Debug.WriteLine("Output " + d.ToString());

					}
					
				}
			}
			return trResults;
		}
		/// <summary>
		/// Runs a feed forward pass on the network.
		/// </summary>
		/// <param name="input">The input vector.</param>
		private void FeedForward(float[] input) 
		{
			if (this.layers.Count > 0) 
			{
				NeuronLayer inputLayer = this.layers[0];
				System.Diagnostics.Debug.Assert(inputLayer != null);

				if (input.Length != inputLayer.Count) 
				{
					throw new ApplicationException("Invalid input pattern size.");
				}
				
				// apply the input pattern to the input layer
				for (int i = 0; i < inputLayer.Count; i++) 
				{
					inputLayer[i].Input = input[i];
				}
				
				// update the output of each layer
				foreach (NeuronLayer l in this.layers) 
				{
					l.ComputeOutput();
				}
			}
		}		
		/// <summary>
		/// Recalls a pattern.
		/// </summary>
		/// <param name="input">The input vector.</param>
		/// <returns>The output vector.</returns>
		public double[] Recall(float[] input) 
		{
			if (this.layers.Count < 1) 
			{
				throw new ApplicationException(
					"Network does not have any layers.");
			}
			FeedForward(input);
			// PrintNetwork();
			// return the output of the last layer
			NeuronLayer outputLayer = this.layers[this.layers.Count - 1];
			if (outputLayer != null) 
			{
				return outputLayer.Output;
			}
			return null;
		}


		/// <summary>
		/// Prints the status of all neurons in the network to the console.
		/// </summary>
//		public void PrintNetwork() 
//		{
//			foreach (NeuronLayer l in this.layers) 
//			{
//				foreach (Neuron n in l) 
//				{
//					System.Console.WriteLine(n);
//				}
//			}
//		}


		/// <summary>
		/// Runs a feeds backward pass through the network.
		/// </summary>
		/// <param name="targetOutput">The target output vector.</param>
		public void FeedBackward(float[] targetOutput) 
		{
			for (int l = layers.Count - 1; l > 0; l--) 
			{
				// output layer
				if (l == layers.Count - 1) 
				{
					// calculate errors for output layer
					for (int n = 0; n < this.layers[l].Count; n++) 
					{
						this.layers[l][n].ComputeError(targetOutput[n]);
					}
				}
				else 
				{
					foreach (Neuron n in this.layers[l]) 
					{
						n.ComputeError();
					}
				}
				
				// adjust weights for connections to each neuron in the 
				// current layer
				foreach (Neuron n in this.layers[l]) 
				{
					double weightChange;
					foreach (Connection c in n.InputConnections)
					{
						// delta rule
						weightChange = this.learnRate * n.OutputError * c.OtherNeuron.Output + this.momentum * c.LastWeightChange;

						c.Weight += weightChange;
						c.LastWeightChange = weightChange;						
					}
					if (n.IsUsingBias) 
					{
						// adjust bias
						weightChange = this.learnRate * n.OutputError + this.momentum * n.LastBiasChange;
						n.BiasWeight += weightChange;
						n.LastBiasChange = weightChange;
					}
				}
			}
		}

		/// <summary>
		/// Starts the training of the neural network.
		/// </summary>
		/// <param name="trainingPatterns">A list with training patterns.</param>
		public void TrainNetwork()
		{
			Debug.Assert(trainingPatterns != null);
			Debug.Assert(trainingPatterns.Count > 0);
			
			Thread t = new Thread(new ThreadStart(TrainThread));
			t.Start();
		}


	
		/// <summary>
		/// Trains the neural network using the given training patterns.
		/// </summary>
		/// <param name="trainingPatterns">A list of training patterns.</param>
		private void TrainThread() 
		{
			
			this.currentError = 100;
			this.isLearning = true;
			int j = -1;
			while (this.currentIteration < this.maximumIterations &&
				this.currentError > this.errorLimit && this.isLearning) 
			{
				j++;
				this.currentIteration++;
				double error = 0F;
				foreach (INeuralPattern p in trainingPatterns) 
				{
					FeedForward(p.Input);
					FeedBackward(p.Output);
					foreach (Neuron n in this.layers[this.layers.Count - 1]) 
					{
						//error += Math.Pow(n.OutputError, 2);
						error += Math.Abs(n.OutputError);

					}
				}
				this.currentError = error / trainingPatterns.Count;

				// trigger event
				if (this.StepFinished != null) 
				{
					this.StepFinished(this, new NetworkNeuralEventArgs(
						this.currentIteration, this.currentError));
				}
				if(j >= 10)
				{
					Debug.WriteLine("Learn iteration " + this.currentIteration.ToString() + " - error is = " + error.ToString());
					j = -1;
				}
			}
			
			if (this.LearningFinished != null) 
			{
				this.LearningFinished(this, null);
			}
	
		}
		/// <summary>
		/// Resets the network by randomizing the weights.
		/// </summary>
		public override void Reset() 
		{
			foreach (NeuronLayer l in this.layers) 
			{
				foreach (Neuron n in l) 
				{
					n.Randomize();
				}
			}
		}
		#endregion
		
		#region serialization
		
		public void GetObjectData(SerializationInfo info, 
			StreamingContext context)
		{
			
			info.AddValue("Layers", this.layers);
			info.AddValue("Momentum", this.momentum);
			info.AddValue("LearnRate", this.learnRate);
			info.AddValue("ErrorLimit", this.errorLimit);
			info.AddValue("MaximumIterations", this.maximumIterations);
		}
		/// <summary>
		/// Loads the network from a file.
		/// </summary>
		/// <param name="filename">The name of the file used for loading.</param>
		/// <returns>A new NetworkBackPropagation object.</returns>
		public static NetworkBackPropagation Load(string filename)
		{
			FileStream fs = new FileStream(filename, FileMode.Open);
			SoapFormatter formatter = new SoapFormatter();
			return (NetworkBackPropagation) formatter.Deserialize(fs);
		}

		/// <summary>
		/// Saves the network to a file.
		/// </summary>
		/// <param name="filename">The name of the file used for saving.</param>
		public void Save(string filename)
		{
			FileStream fs = new FileStream(filename, FileMode.Create);
			SoapFormatter formatter = new SoapFormatter();
			formatter.Serialize(fs, this);
		}
		#endregion
	}
}
